package epam.lab.spring.beanfactory;

import epam.lab.spring.parser.BeanDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Bean Factory to create objects from bean definitions
 * @author Iurii Miedviediev
 * @version 1.0 Build 22.08.2014
 */
public class StandardBeanFactory implements BeanFactory {

    private Map<String, BeanDefinition> beanDefs;
    private static final Logger LOGGER = LoggerFactory.getLogger(StandardBeanFactory.class);

    public StandardBeanFactory(Map<String, BeanDefinition> beanDefs) {
        this.beanDefs = beanDefs;
    }
    public StandardBeanFactory() {
        beanDefs = new HashMap<>();
    }

    /**
     * Add new BeanDefinition to the factory
     */
    public void registerDefinition(String id, BeanDefinition beanDef) {
        this.beanDefs.put(id, beanDef);
    }

    /**
     * Get Bean from the factory by specified id
     * @param id id of the wanted bean
     * @return bean object
     */
    @Override
    public Object getBean(String id) {
        Object bean = null;
        BeanDefinition definition = beanDefs.get(id);

        try {
            Class<?> clazz = Class.forName(definition.getaClass());
            if(definition.getConstructorArgs().isEmpty()) {
                bean = clazz.newInstance();
            } else {
                Object[] args = prepareConstructorArguments(definition.getConstructorArgs());
                Constructor constructor = clazz.getConstructor(this.getConstructorArgsClasses(definition.getConstructorArgs()));
                bean = constructor.newInstance(args);
            }
            setProperties(bean, definition.getProperties());

        } catch (ClassNotFoundException |
                InstantiationException |
                IllegalAccessException |
                InvocationTargetException |
                NoSuchMethodException |
                NoSuchFieldException e) {

            LOGGER.error("Some of the fields are wrong", e);
            throw new IllegalArgumentException("Some of the fields are wrong. Check your xml file");
        }
        return bean;
    }

    /**
     * Set all properties to the bean through reflections
     * @param bean bean to set the properties
     * @param properties map of properties
     * @throws NoSuchFieldException
     * @throws ClassNotFoundException
     * @throws IllegalAccessException
     */
    private void setProperties(Object bean, Map<String, String> properties)
            throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {

        for(Map.Entry<String, String> property : properties.entrySet()) {
            String[] values = property.getValue().split("-");
            Object val = values[1];

            Field field = bean.getClass().getDeclaredField(property.getKey());

            field.setAccessible(true);

            if("ref".equals(values[0])) {
                Class refBeanClass = Class.forName(beanDefs.get(values[1]).getaClass());
                val = this.getBean(values[1], refBeanClass);
            }
            field.set(bean, val);
            field.setAccessible(false);
        }
    }

    /**
     * Get array of constructor arguments' classes
     * @param args list of arguments
     * @return array of classes correspond to the arguments
     * @throws ClassNotFoundException
     */
    private Class[] getConstructorArgsClasses(List<String> args) throws ClassNotFoundException {
        List<Class> classes = new ArrayList<>();
        for(String arg : args) {
            String[] splitted = arg.split("-");
            if("value".equals(splitted[0])) {
                classes.add(String.class);
            } else {
                classes.add(Class.forName(this.beanDefs.get(splitted[1]).getaClass()));
            }
        }
        return classes.toArray(new Class[classes.size()]);
    }

    /**
     * Convert constructor arguments from definitions into real arguments
     * @param args list of arguments' definitions
     * @return array of arguments, objects
     */
    private Object[] prepareConstructorArguments(List<String> args) {
        List<Object> parsedArgs = new ArrayList<>();
        for(String arg : args) {
            String[] splitted = arg.split("-");
            if("value".equals(splitted[0])) {
                parsedArgs.add(splitted[1]);
            } else {
                parsedArgs.add(this.getBean(splitted[1]));
            }
        }
        return parsedArgs.toArray();
    }

    /**
     * Get bean of specified type from the factory
     * @param string id of the wanted bean
     * @param type class of the bean
     * @param <T> type of the bean
     * @return wanted bean, casted to the specified type
     */
    @Override
    public <T> T getBean(String string, Class<T> type) {
        return (T) this.getBean(string);
    }
}
